package com.gmrz.uaf.remote.protocol.v1.processor;

import com.gmrz.service.challengestore.ChallengeStoreException;
import com.gmrz.service.challengestore.ChallengeStoreService;
import com.gmrz.uaf.common.*;
import com.gmrz.uaf.crypto.CertUtil;
import com.gmrz.uaf.crypto.CryptoConstants;
import com.gmrz.uaf.crypto.spi.UAFAuthAlgorithmSuite;
import com.gmrz.uaf.db.DAOException;
import com.gmrz.uaf.db.UAFDAOFactory;
import com.gmrz.uaf.db.UAFDBConnectionMgr;
import com.gmrz.uaf.db.dao.AuthenticatorDAO;
import com.gmrz.uaf.policy.verification.PolicyVerificationException;
import com.gmrz.uaf.protocol.v1.processor.UAFAuthFinishProcessor;
import com.gmrz.uaf.protocol.v1.processor.exception.*;
import com.gmrz.uaf.protocol.v1.schema.*;
import com.gmrz.uaf.protocol.v1.validaton.ValidationException;
import com.gmrz.uaf.protocol.v1.validaton.auth.AuthSignAssertionValidator;
import com.gmrz.uaf.remote.plugin.AuthServicePluginManager;
import com.gmrz.uaf.server.ServerContext;
import com.gmrz.uas.plugin.caservice.CertServicePlugin;
import com.gmrz.uas.plugin.caservice.bean.P7Bean;
import com.gmrz.uas.plugin.exception.PluginException;
import com.gmrz.util.Convert;
import com.gmrz.util.CryUtil;
import com.gmrz.util.Strings;
import com.gmrz.util.db.DBUtil;
import com.gmrz.uaf.crypto.sm.SM2Utils;
import com.google.inject.Inject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.KeyFactory;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;

public class UAPCertAuthFinishProcessor extends UAFAuthFinishProcessor {

    private static final Logger LOG = LogManager.getLogger(UAPCertAuthFinishProcessor.class);
    AuthServicePluginManager serviceManager;

    @Inject
    public void setServiceManager(AuthServicePluginManager serviceManager) {
        this.serviceManager = serviceManager;
    }

    @Override
    protected Authenticator processAssertion(AuthenticationResponse r, AuthenticationAssertion assertion,
                                             Authenticator authenticator, String ext,String functionType)
            throws ValidationException, DAOException, PolicyVerificationException, MetadataNotFoundException,
            TLVParsingException, CertificateValidationException, SignatureValidationException,
            TxnContentValidationException, IOException, ChallengeStoreException, ExactMatchNotFoundException, AdditionUviException, PluginException {
        authenticator.getMetadata().setAuthType(authenticator.getAuthType());
        ServerData serverData = r.getOperationHeader().getServerData();

        String challenge = Convert.toBase64(r.getFinalChallengeParams().getServerChallenge().getChallenge());
        ChallengeStoreService challengeStoreService = GuiceUtil.getProcessorInjector().getInstance(ChallengeStoreService.class);
        String val = challengeStoreService.getRedisValue(Constants.UAP_ACTIVITY_CACHE_KEY + challenge + "-" + authenticator.getUserName());


        if(authenticator.getCertificateExtendBean().getCertStatus() < Constants.CERT_STATUS_3){
            if(!"1".equals(val)) {
                throw new SignatureValidationException(UAFErrorCode.PROTOCOL_VALIDATE_CERT_STATUS_FAILED);
            }
        }else if(authenticator.getCertificateExtendBean().getCertStatus() > Constants.CERT_STATUS_3){
            throw new SignatureValidationException(UAFErrorCode.PROTOCOL_FROZEN_CERT_STATUS_FAILED);
        }
        if (this.getServerConfig().getPerformModel() < Constants.PERFORM_MODEL_LEVEL_3 && !ServerContext.getServerContext().isPerform()) {
            AuthSignAssertionValidator asv = new AuthSignAssertionValidator(authenticator, r.getFCPBase64(), serverData);
            // seeertion断言数据的校验
            asv.validate(assertion);
        }
        String strKeyID = Convert.toBase64(assertion.getSignResponse().getSignData().getKeyID());
        String aaid = assertion.getSignResponse().getSignData().getAAID();
        // 断言方案
        String scheme = assertion.getSignDataScheme();
        LOG.info("Sign data scheme: {}", scheme);
        // 从断言中获取 签名响应
        ASMSignResponse response = assertion.getSignResponse();
        // 从签名响应中获取 待签名 的原数据
        SignData signData = response.getSignData();
        // 从签名响应中获取 签名
        byte[] signature = response.getSignature();
        // 从 待签名 的原数据 中获取 签名算法
        int signAlgorithm = signData.getSignAlgorithm();
        String krdDeviceID = signData.getDeviceID();
        String deviceID = authenticator.getDeviceID();
        if(Strings.isNotBlank(krdDeviceID)){
            if(!krdDeviceID.equals(deviceID.substring(2))){
                LOG.error("validate krd deviceID error krdDeviceID:"+krdDeviceID+",deviceID:"+deviceID);
                throw new SignatureValidationException(UAFErrorCode.PROTOCOL_VALIDATE_DEVICEID_FAILED);
            }
        }

        TLVType2 signedDataType = new TLVType2() {
        };
        signedDataType.addTLVSegment((short) 21767,
                signData.getReceivedBytes());
        byte[] signedData = signedDataType.getTLVBytes();
        // 从数据库中查询到的认证数据中的公钥
        String strPubKey = authenticator.getPubKey();
        if (Strings.isNullOrEmpty(strPubKey)) {
            LOG.error("Failed to locate public key associate with AAID[{}], keyID[{}]",
                    aaid, strKeyID);
            throw new AuthenticationFailedException("Failed to locate public key associate with AAID[" + aaid + "]");
        }
        byte[] pubKey = Convert.fromBase64(authenticator.getPubKey());
        if (this.getServerConfig().getPerformModel() < Constants.PERFORM_MODEL_LEVEL_3 && !ServerContext.getServerContext().isPerform()) {
            boolean useSM2flag = false;
            byte[] tagCertIsDeviceKeySign = signData.getTagCertIsDeviceKeySign();
            if(null != tagCertIsDeviceKeySign) {
                if (Convert.byteArrayToInt(tagCertIsDeviceKeySign) == 1) {
                    String devicePublicKeyStr = null;
                    List<Extension> devicePublicKeyExtensions = authenticator.getMetadata().getDevicePublicKeyExtensions();
                    if (null != devicePublicKeyExtensions) {
                        Extension devicePublicKeyExtension = devicePublicKeyExtensions.get(0);
                        if (null != devicePublicKeyExtension) {
                            devicePublicKeyStr = devicePublicKeyExtension.getData();
                        }
                    }
                    if (Strings.isNullOrEmpty(devicePublicKeyStr)) {
                        throw new SignatureValidationException(UAFErrorCode.PROTOCOL_VALIDATE_KRD_FAILED);
                    }
                    try {
                        if (authenticator.getMetadata().getAlgorithm() == CryptoConstants.UAFAlgSign.UAF_ALG_SIGN_SM2.getSuite()) {
                            //sm
                            pubKey = CertUtil.getPublicKey(Convert.fromBase64(devicePublicKeyStr));
                        } else {
                            //rsa
                            CertificateFactory cf = CertificateFactory.getInstance("X.509");
                            X509Certificate cert = (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(Convert.fromBase64(devicePublicKeyStr)));
                            RSAPublicKey rsaPub = (RSAPublicKey) cert.getPublicKey();
                            byte[] modulus = rsaPub.getModulus().toByteArray();
                            byte[] e = rsaPub.getPublicExponent().toByteArray();
                            pubKey = Arrays.copyOfRange(modulus, 1, modulus.length);
                            byte[] pubKeyBytes = new byte[pubKey.length+e.length];
                            System.arraycopy(pubKey,0,pubKeyBytes,0,pubKey.length);
                            System.arraycopy(e,0,pubKeyBytes,pubKey.length,e.length);
                            pubKey = pubKeyBytes;
                        }
                    } catch (Exception e) {
                        LOG.error("get sm2 certificate from string error", e);
                    }
                    LOG.debug("get device public key from metadata :"+Convert.toHex(pubKey));
                }else if (Convert.byteArrayToInt(tagCertIsDeviceKeySign) == 2) {
                    useSM2flag = true;
                    pubKey = Convert.fromBase64(authenticator.getFidoPubkey());
                }
            }
            if(useSM2flag){
                sm(pubKey, signedData, signature);
            }else {
                if(signAlgorithm == CryptoConstants.UAFAlgSign.UAF_ALG_SIGN_SM2.getSuite()) {
                    sm(pubKey, signedData, signature);
                }else{
                    rsa(pubKey, signedData, signature,signAlgorithm,authenticator);
                }
            }

            LOG.info("Successfully verified signature for username[{}]", authenticator.getUserName());
        }
        if(Strings.isNotBlank(ext)){
            boolean flag = validateExtHash(challenge,authenticator.getUserName(),ext);
            if(!flag){
                throw new SignatureValidationException(UAFErrorCode.PROTOCOL_VALIDATE_P7_CONTEXT_HASH_FAILED);
            }
        }

        byte[] p7Data = response.getP7Bytes();
        String validateTranHash = serverData.getValidateDataHash(Constants.CERT_TRANSACTION_TEXT + authenticator.getAAID().format());
        String sdShowFlagHash = serverData.getValidateDataHash(Constants.CERT_TRANSACTION_SHOWFLAG + authenticator.getAAID().format());
        String sdShowTextHash = serverData.getValidateDataHash(Constants.CERT_TRANSACTION_SHOWTEXT + authenticator.getAAID().format());
        String sdExtTestHash = serverData.getValidateDataHash(Constants.CERT_TRANSACTION_EXT_TEXT + authenticator.getAAID().format());

        String assertionValidateTranHash = Convert.toBase64(assertion.getSignResponse().getSignData().getTransactionTextHash());
        if(!assertionValidateTranHash.equals(validateTranHash)){
            throw new SignatureValidationException(UAFErrorCode.PROTOCOL_VALIDATE_P7_CONTEXT_HASH_FAILED);
        }
        if (!validateTransaction(p7Data, validateTranHash,signAlgorithm,signData,sdShowFlagHash,sdShowTextHash,sdExtTestHash)) {
            throw new SignatureValidationException(UAFErrorCode.PROTOCOL_VALIDATE_P7_CONTEXT_HASH_FAILED);
        }

        CertServicePlugin certServicePlugin = serviceManager.getCertServicePlugin( authenticator.getAuthType());
        P7Bean p7Bean = new P7Bean();
        p7Bean.setP7(p7Data);
        p7Bean.setExt(ext);
        p7Bean.setUserName(authenticator.getUserName());
        p7Bean.setAlgorithm(signAlgorithm);
        p7Bean.setAppID(authenticator.getAppID());
        p7Bean.setAuthType(authenticator.getAuthType());
        p7Bean.setTransType(authenticator.getTransType());
        boolean verifyRes = certServicePlugin.verifySignature(p7Bean);
        if (!verifyRes) {
            throw new SignatureValidationException(UAFErrorCode.PROTOCOL_SIGNATURE_VALIDATION_FAILED);
        }
        List<Extension> exts = assertion.getExtensions();
        if ((exts != null) && (!exts.isEmpty())) {
            processExtensions(exts);
        }

        int signCounter = signData.getSignCounter();
        authenticator.setSignCounter(signCounter);
        Connection conn = null;
        try {
            conn = UAFDBConnectionMgr.getConnection(true);
            AuthenticatorDAO adao = UAFDAOFactory.createAuthenticatorDAO(conn);
            // 更新认证数据表中的 验签数
            int rowsupdated = adao.updateSignCounter(signCounter, authenticator.getAppID(), authenticator.getUserName(), aaid, strKeyID);
            if (rowsupdated != 1) {
                LOG.error("Failed to update sign counter for AAID [{}], keyID[{}]", aaid, strKeyID);
                throw new AuthenticationFailedException("Failed to update sign counter for AAID[" + aaid + "]");
            }
        } catch (SQLException sqle) {
            throw new DAOException(UAFErrorCode.DB_DATABASE_CONNECTION_ERROR, sqle);
        } finally {
            DBUtil.close(conn);
        }
        LOG.debug("Successfully updated the signCounter for AAID[{}] to value [{}]",
                aaid, Integer.valueOf(signCounter));

        super.processExactMatch(signData.getUvi(), authenticator);

        return authenticator;
    }

    private boolean validateExtHash(String challenge, String hashedUserName, String ext) {
        String extHash = Convert.toBase64(CryUtil.getSHA256(Convert.fromBase64(ext)));
        ChallengeStoreService uafChallengeStoreService = this.getmUafChallengeStoreService();
        try {
            String cacheExtHash = uafChallengeStoreService.getRedisValue(Constants.UAF_CERT_AUTH_EXT_HASH + challenge + "-" + hashedUserName);
            if(Strings.isNotBlank(cacheExtHash)){
                if(cacheExtHash.equals(extHash)){
                    return true;
                }
            }
        } catch (ChallengeStoreException e) {
            throw new UAFCommonRuntimeException(e.getMessage());
        }
        return false;
    }

    private void rsa(byte[] pubKey, byte[] signedData, byte[] signature,int signAlgorithm,Authenticator authenticator) throws SignatureValidationException {
        boolean signFlag = false;
        byte[] pubexp = {(byte)0x00,(byte)0x01,(byte)0x00,(byte)0x01};
        try {
            byte[] signedKRDBytesP1 = new byte[51];
            CryUtil.getRsaPkcs1PaddingDta(CryUtil.getSHA256(signedData), signedKRDBytesP1, (byte) 1);

            KeyFactory factory = KeyFactory.getInstance("RSA");
            BigInteger n = new BigInteger(1, Arrays.copyOfRange(pubKey, 0, 256));
            BigInteger e = new BigInteger(1, pubexp);
            byte[] nba = n.toByteArray();
            byte[] eba = e.toByteArray();
            LOG.debug("Decoded N({}) - {}", Integer.valueOf(nba.length), Arrays.toString(nba));
            LOG.debug("Decoded E({}) - {}", Integer.valueOf(eba.length), Arrays.toString(eba));
            RSAPublicKeySpec spec = new RSAPublicKeySpec(n, e);
            RSAPublicKey pub = (RSAPublicKey)factory.generatePublic(spec);
            LOG.debug("pubkey:"+Convert.toBase64(pubKey));
            LOG.debug("signedKRDBytes:"+Convert.toBase64(signedData));
            LOG.debug("signedKRDBytesP1:"+Convert.toBase64(signedKRDBytesP1));
            LOG.debug("signature:"+Convert.toBase64(signature));
            UAFAuthAlgorithmSuite uafAuthAlgorithmSuite = cryptoEngine.getAuthAlgorithmSuite(signAlgorithm);
            signFlag = uafAuthAlgorithmSuite.verifySignature(pub, signedKRDBytesP1, signature);
        }catch (Exception e){
            LOG.error("rsa error",e);
        }
        if (!signFlag) {
            throw new SignatureValidationException(UAFErrorCode.PROTOCOL_SIGNATURE_VALIDATION_FAILED);
        }

    }

    private void sm(byte[] pubKey, byte[] signedData, byte[] signature) throws SignatureValidationException {
        boolean signFlag = SM2Utils.verifySignRaw(pubKey, signedData, signature);
        if (!signFlag) {
            throw new SignatureValidationException(UAFErrorCode.PROTOCOL_SIGNATURE_VALIDATION_FAILED);
        }
    }

    private boolean validateTransaction(byte[] p7Data, String validateTranHash,int signAlgorithm,SignData signData,String sdShowFlagHash,String sdShowTextHash,String sdExtTestHash) {
        try {
            String signDataShowTextHashBase64 = Convert.toBase64(signData.getCertAuthTradeShowdataHash());
            String signDataShowFlagBase64 = Convert.toBase64(signData.getCertAuthTradeIshow());

            byte[] data = SM2Utils.getData(p7Data);
            UAFAuthAlgorithmSuite uafAuthAlgorithmSuite = cryptoEngine.getAuthAlgorithmSuite(signAlgorithm);
            String p7ExtTextHashBase64 = Convert.toBase64(uafAuthAlgorithmSuite.hash(data));
            if("AAA".equals(sdShowFlagHash) && Strings.isNullOrEmpty(sdShowTextHash)){
                if (sdShowFlagHash.equals(signDataShowFlagBase64) && p7ExtTextHashBase64.equals(sdExtTestHash)) {
                    return true;
                }
            }
            if (sdShowFlagHash.equals(signDataShowFlagBase64)
                    && sdShowTextHash.equals(signDataShowTextHashBase64) && p7ExtTextHashBase64.equals(sdExtTestHash)) {
                return true;
            }

        } catch (Exception e) {
            LOG.error("validateTransaction error", e);
        }
        return false;

    }
    private CertAuthTransaction getCertAuthTransaction(byte[] valBytes){
        ByteBuffer certAuthTransactionBuffer = ByteBuffer.wrap(valBytes).order(ByteOrder.LITTLE_ENDIAN);
        CertAuthTransaction certAuthTransaction = new CertAuthTransaction();
        certAuthTransaction.setReceivedBytes(valBytes);
        while (certAuthTransactionBuffer.hasRemaining()) {
            byte[] tagByte = new byte[2];
            certAuthTransactionBuffer.get(tagByte);
            int tag = Convert.byteArrayToInt(tagByte);
            byte[] lenByte = new byte[2];
            certAuthTransactionBuffer.get(lenByte);
            int len = Convert.byteArrayToInt(lenByte);
            byte[] val;
            switch (tag) {
                case 35330:
                    val = new byte[len];
                    certAuthTransactionBuffer.get(val);
                    certAuthTransaction.setCertAuthTradeBase(val);
                    break;
                case 35331:
                    val = new byte[len];
                    certAuthTransactionBuffer.get(val);
                    certAuthTransaction.setCertAuthTradeIshow(val);
                    break;
                case 35332:
                    val = new byte[len];
                    certAuthTransactionBuffer.get(val);
                    certAuthTransaction.setCertAuthTradeShowdata(val);
                    break;
            }
        }
        return certAuthTransaction;
    }

}
